Skip to content

11 函数

Xmind

function xmind

为什么需要函数

  1. 为什么需要函数?
    • 可以实现代码复用,提高开发效率
  2. 函数是什么?
    • function 设计为执行特定任务的代码块
  • 函数:function,是被设计为执行特定任务的代码块

  • 说明:函数可以把具有相同或相似逻辑的代码 " 包裹 " 起来,通过函数调用执行这些被 " 包裹 " 的代码逻辑,这么做的优势是有利于精简代码方便复用

    比如我们前面使用的 alert()prompt()console.log() 都是一些 js 函数,只不过已经封装好了,我们直接使用的。

  • 作用:

    • 代码重用:使用函数可以将一组操作封装成一个单元,从而使其可重用。这种方式可以大大减少代码量,并提高代码的可维护性。
    • 模块化:通过将代码分解为多个函数,可以将代码组织成模块化代码,从而使代码更易于理解和维护。
    • 抽象层次:函数可以作为抽象层次,隐藏底层实现细节并提供更高级别的接口。这使得代码更加简洁、易于阅读和测试。
    • 参数传递:通过函数参数,可以将真实数据传递给函数,这样就可以将数据处理和计算逻辑与其他部分的代码分离开来,从而提高了代码的可读性和可维护性。
    • 作为回调函数:JavaScript 函数可以被用作回调函数,即在异步程序执行完毕后,在特定条件下调用的函数。这种方式可以灵活地控制程序的流程,从而改进用户体验。

函数使用

  1. 函数是用那个关键字声明的?
    • function
  2. 函数不调用会执行吗?如何调用函数?
    • 函数不调用自己不执行
    • 调用方式:函数名()
  3. 函数的复用代码和循环重复代码有什么不同?
    • 循环代码写完即执行,不能很方便控制执行位置
    • 随时调用,随时执行,可重复调用

函数的声明语法

js
// 声明关键字  函数名  形式参数列表
function functionName(parameter1, parameter2, parameterN) {
  // 函数体
  return result;
}
  • function 关键字表示函数定义的开始,后面是函数名称、圆括号和函数体。
  • 在圆括号中可以列出该函数需要的零个或多个参数,用逗号分隔。
  • 函数体包含一组语句,定义了函数执行的操作。
  • 最后,通过 return 语句返回函数结果。

函数名命名规范

  • 和变量命名基本一致
  • 尽量小驼峰式命名法
  • 前缀应该为动词
  • 命名建议:常用动词约定
  • 函数名称应该能够清楚地表达该函数的功能,并遵守标识符的命名规则,即只能由字母、数字、下划线和美元符号组成,不能以数字开头,不允许使用 JavaScript 中的关键字和保留字作为函数名称。
  • 通常采用驼峰式(camelCase)的命名规范,即第一个单词小写,其余单词首字母大写,例如 addNumbers。如果函数名称由多个单词组成,则可以使用下划线或破折号来分隔单词,例如 calculate_areashow-alert
动词含义
can判断是否可执行某个动作
has判断是否含义某个值
is判断是否为某个值
get获取某个值
set设置某个值
load加载某些数据
js
function getName() {}
function addSquares() {}

函数的调用语法

JavaScript 函数可以通过函数名称来调用。调用函数时需要指定函数的参数列表(如果该函数需要参数)。

js
functionName(arg1, arg2, arg3, ..., argN);

// 如果要调用一个名为 addNumbers 的函数,并将数字 3 和 4 作为参数传递给它
addNumbers(3, 4);
// 将 addNumbers 函数的返回值赋值给变量 sum
var sum = addNumbers(3, 4);
  • functionName 是函数名称,arg1argN 是传递给函数的参数列表,用逗号分隔。

我们曾经使用的 alert() , parseInt() 这种名字后面跟小括号的本质都是函数的调用。

注意

  • 声明(定义)的函数必须调用才会真正被执行,使用 () 调用函数。
  • 函数名的命名规则与变量是一致的,并且尽量保证函数名的语义。

函数体

函数体是函数的构成部分,它负责将相同或相似代码 " 包裹 " 起来,直到函数调用时函数体内的代码才会被执行。

函数的功能代码都要写在函数体当中。

练习 - 函数课堂练习

需求:

  1. 写一个打招呼的函数 hi~
  2. 把 99 乘法表封装到函数里面,重复调用 3 次
js
// 打招呼的函数 hi~
function sayHi(name) {
  console.log(`Hi, ${name}!`);
}

sayHi("Tom"); // 输出 Hi, Tom!

// 把 99 乘法表封装到函数里面,重复调用 3 次
function multiplicationTable() {
  for (let i = 1; i <= 9; i++) {
    let row = "";
    for (let j = 1; j <= i; j++) {
      row += `${j}*${i}=${i * j}\t`;
    }
    console.log(row);
  }
}

for (let i = 0; i < 3; i++) {
  multiplicationTable();
}
练习 - 函数案例

需求:

  1. 封装一个函数,计算两个数的和
  2. 封装一个函数,计算 1-100 之间所有数的和
js
// 封装一个函数,计算两个数的和
function sum(a, b) {
  return a + b;
}

console.log(sum(2, 3)); // 输出 5

// 封装一个函数,计算 1-100 之间所有数的和
function sumOfNumbers() {
  let sum = 0;
  for (let i = 1; i <= 100; i++) {
    sum += i;
  }
  return sum;
}

console.log(sumOfNumbers()); // 输出 5050

函数传参

  1. 函数传递参数的好处是?
    • 可以极大的提高了函数的灵活性
  2. 函数参数可以分为那两类?怎么判断他们是那种参数?
    • 函数可以分为形参和实参
    • 函数声明时,小括号里面的是形参,形式上的参数
    • 函数调用时,小括号里面的是实参,实际的参数
    • 尽量保持形参和实参的个数一致
  3. 参数中间用什么符号隔开?
    • 逗号

声明(定义)一个功能为打招呼的函数

  • 传入数据列表
  • 声明这个函数需要传入几个数据
  • 多个数据用逗号隔开

参数

通过向函数传递参数,可以让函数更加灵活多变,参数可以理解成是一个变量。

  1. 声明(定义)函数时的形参没有数量限制,当有多个形参时使用 , 分隔
  2. 调用函数传递的实参要与形参的顺序一致

形参和实参

js
// x, y 形参
function addNumbers(x, y) {
  return x + y;
}

// 3, 4 实参
let result = addNumbers(3, 4);
console.log(result); // 输出 7
  • 形参:声明函数时写在函数名右边小括号里的叫形参(形式上的参数)
  • 实参:调用函数时写在函数名右边小括号里的叫实参(实际上的参数)
  • 形参可以理解为是在这个函数内声明的变量(比如 num1 = 10)实参可以理解为是给这个变量赋值
  • 开发中尽量保持形参和实参个数一致
  • 我们曾经使用过的 alert('打印'), parseInt('11'), Number('11') 本质上都是函数调用的传参。

注意

  • 函数调用中传递的实参数量必须与函数定义中指定的形参数量相同,否则会导致错误。
  • 如果未传递所有形参,则缺少的形参将设置为 undefined
练习 - 函数封装求和

需求:采取函数封装的形式:输入 2 个数,计算两者的和,打印到页面中

js
function getSum(a, b) {
  document.write(a + " + " + b + " = " + (a + b));
}

let num1 = +prompt("请输入第一个数");
let num2 = +prompt("请输入第二个数");

getSum(num1, num2);

参数默认值

形参可以看做变量,但是如果一个变量不给值,默认是 undefined

但是如果做用户不输入实参,刚才的案例,则出现 undefined + undefined 结果是 NaN

我们可以改进下,用户不输入实参,可以给 形参默认值,可以默认为 0, 这样程序更严谨,可以如下操作:

js
function getSum(a = 0, b = 0) {
  return a + b;
}

getSum(); // 输出为 0,而不是 NaN
getSum(1, 2); // 输出为 3

注意

函数形参默认值只会在缺少实参参数传递时 才会被执行,所以有参数会优先执行传递过来的实参,否则默认为 undefined

练习 - 函数封装 - 求学生总分
  • 需求:学生的分数是一个数组,计算每个学生的总分
  • 分析:
    • 封装一个求和函数
    • 传递过去的参数是一个数组
    • 函数内部遍历数组求和
js
// 封装一个求和函数,形参是一个数组
function sum(nums) {
  let total = 0;
  // 函数内部遍历数组求和
  for (let i = 0; i < nums.length; i++) {
    total += nums[i];
  }
  return total;
}

let scores = [
  [80, 90, 70],
  [75, 95, 85],
  [90, 80, 85],
];

for (let i = 0; i < scores.length; i++) {
  // 传递过去的参数是一个数组
  let studentTotal = sum(scores[i]);
  console.log(`Student ${i + 1} total score: ${studentTotal}`);
}

// Student 1 total score: 240
// Student 2 total score: 255
// Student 3 total score: 255

函数返回值

  1. 函数很多情况下需要返回值
  2. 为什么要让函数有返回值
    • 函数执行后得到结果,结果是调用者想要拿到的(一句话,函数内部不需要输出结果,而是返回结果)
    • 对执行结果的扩展性更高,可以让其他的程序使用这个结果
  3. 函数有返回值用那个关键字?有什么注意事项呢?
    • 语法:return 数据
    • return 后面不接数据或者函数内不写 return,函数的返回值是 undefined
    • return 能立即结束当前函数,所以 return 后面的数据不要换行写
  • 函数的本质是封装(包裹),函数体内的逻辑执行完毕后,函数外部如何获得函数内部的执行结果呢?
  • 要想获得函数内部逻辑的执行结果,需要通过 return 这个关键字,将内部执行结果传递到函数外部,这个被传递到外部的结果就是返回值。
js
function addNumbers(x, y) {
  return x + y;
}

let result = addNumbers(3, 4);
console.log(result); // 输出 7

// 当然,我们也可以直接使用函数调用表达式,而不是先将结果存储在变量中。
console.log(addNumbers(3, 4)); // 输出 7

总结

  1. 在函数体中使用 return 关键字能将内部的执行结果交给函数外部使用。
  2. 函数内部只能出现 1 次 return,并且 return 下一行代码不会再被执行,所以 return 后面的数据不要换行写。
  3. return 会立即结束当前函数。
  4. 函数可以没有 return,这种情况默认返回值为 undefined。
函数返回值练习 - 求任意 2 个数中的最大值,并返回

求任意 2 个数中的最大值,并返回。

js
function getMaxNum(num1, num2) {
  return num1 > num2 ? num1 : num2;
}

console.log(getMaxNum(5, 9)); // 输出 9
函数返回值练习 - 求任意数组中的最大值并返回这个最大值

求任意数组中的最大值并返回这个最大值。

js
function getMaxInArray(arr) {
  let max = arr[0];
  for (let i = 1; i < arr.length; i++) {
    if (arr[i] > max) {
      max = arr[i];
    }
  }
  return max;
}

console.log(getMaxInArray([5, 9, 3, 7, 6])); // 输出 9
函数返回值练习 - 求任意数组中的最小值并返回这个最小值

求任意数组中的最小值并返回这个最小值。

js
function getMinInArray(arr) {
  let min = arr[0];
  for (let i = 1; i < arr.length; i++) {
    if (arr[i] < min) {
      min = arr[i];
    }
  }
  return min;
}

console.log(getMinInArra([5, 9, 3, 7, 6])); // 输出 3

断点调试:进入函数内部看执行过程 F11

函数细节补充

  • 两个相同的函数后面的会覆盖前面的函数

  • 在 Javascript 中 实参的个数和形参的个数可以不一致

    • 如果形参过多 会自动填上 undefined (了解即可)
    • 如果实参过多 那么多余的实参会被忽略 (函数内部有一个 arguments,里面装着所有的实参)
  • 函数一旦碰到 return 就不会在往下执行了 函数的结束用 return

break 和 return 区别

break 和 return 都可以用来结束代码块的执行。

  • break 停止循环或 switch 语句中的执行,并跳出该语句。
  • return 结束当前函数的执行并返回一个值(如果提供了值)。
js
function findIndex(arr, value) {
  for (let i = 0; i < arr.length; i++) {
    if (arr[i] === value) {
      return i; // 如果找到了值,返回下标并结束函数的执行
    }
  }
  return -1; // 如果未找到值,则返回 -1 并结束函数的执行
}

let numbers = [3, 6, 9, 12, 15];
let index = findIndex(numbers, 12);
console.log(index); // 输出 3
  • 定义了一个名为 findIndex 的函数,该函数接受一个数组和一个值作为参数,并返回该值在数组中的下标。
  • 使用一个 for 循环来遍历数组,并在找到值时使用 return 语句立即返回下标并结束函数的执行。
  • 如果循环结束时仍未找到值,则返回 -1 并结束函数的执行。

注意

  • break 只能用于循环语句(如 for、while、do-while),而不能用于其他类型的代码块。
  • return 不仅可以用于函数中,还可以用于其他类型的代码块中(如一个函数嵌套在另一个函数中)。

作用域

  1. JS 中作用域分为哪 2 种?
    • 全局作用域。函数外部或者整个 script 有效
    • 局部作用域。也称为函数作用域,函数内部有效
  2. 根据作用域不同,变量分为哪 2 种?
    • 全局变量
    • 局部变量
  3. 有一种特殊情况是全局变量是那种?我们提倡吗?
    • 局部变量或者块级变量 没有 let 声明直接赋值的当全局变量看
    • 我们强烈不提倡
    • 还有一种特殊情况,函数内部的形参可以当做局部变量看
  4. 变量访问原则是什么?
    • 采取就近原则的方式来查找变量最终的值

通常来说,一段程序代码中所用到的名字并不总是有效和可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域。

作用域的使用提高了程序逻辑的局部性,增强了程序的可靠性,减少了名字冲突。

全局作用域

  • 作用于所有代码执行的环境 (整个 script 标签内部) 或者一个独立的 js 文件

  • 处于全局作用域内的变量,称为全局变量

    • 函数外部 let 的变量
    • 全局变量在任何区域都可以访问和修改

局部作用域

  • 作用于函数内的代码环境,就是局部作用域。因为跟函数有关系,所以也称为函数作用域。

  • 处于局部作用域内的变量称为局部变量

    • 函数内部 let 的变量
    • 局部变量只能在当前函数内部访问和修改

变量特殊情况

  • 如果函数内部,变量没有声明,直接赋值,也当全局变量看,但是强烈不推荐

  • 但是有一种情况,函数内部的形参可以看做是局部变量。

变量的访问原则

  • 只要是代码,就至少有一个作用域
  • 写在函数内部的局部作用域
  • 如果函数中还有函数,那么在这个作用域中就又可以诞生一个作用域
  • 访问原则:在能够访问到的情况下 先局部,局部没有在找全局
  • 作用域链:采取就近原则的方式来查找变量最终的值。
案例练习 - 变量的访问原则

作用域链:采取就近原则的方式来查找变量最终的值。

js
function f1() {
  let num = 123; // num
  function f2() {
    let num = 0; // 就近 num
    console.log(num); // num 输出 0
  }
  f2();
}
let num = 456;
f1();
js
let a = 1; // a
function fn1() {
  let a = 2; // a
  let b = "22"; // 就近 b
  fn2();
  function fn2() {
    let a = 3; // a
    fn3();
    function fn3() {
      let a = 4; // 就近 a
      console.log(a); // a 的值 4
      console.log(b); // b 的值 22
    }
  }
}
fn1();

匿名函数

  1. 立即执行函数有什么作用?
    • 防止变量污染
  2. 立即执行函数需要调用吗?有什么注意事项呢?
    • 无需调用,立即执行,其实本质已经调用了
    • 多个立即执行函数之间用分号隔开

函数可以分为具名函数和匿名函数。

  • 具名函数:具名函数是指在函数定义时为函数指定一个名称。

    • 具名函数在代码中很容易识别和调试,因为它们有一个明确的名称,而且可以在整个程序中重复使用。
    • 可以通过函数名调用该函数。
    js
    function add(x, y) {
      return x + y;
    }
    
    // 通过函数名调用该函数
    let result = add(2, 3);
    console.log(result); // 输出 5
  • 匿名函数:匿名函数是指在函数定义时不指定函数名称的函数。这种函数通常作为参数传递给其他函数或被赋值给变量。

    • 匿名函数可以作为函数表达式来使用,也可以作为回调函数传递给其他函数。它们通常用于编写更简洁、可读性更强的代码,并且不会污染全局命名空间。
    • 没有名字的函数,无法直接使用。
    JS
    let add = function(x, y) {
      return x + y;
    };
    
    // let result = add(2, 3);
    console.log(result); // 输出 5

    注意

    除非将匿名函数赋值给变量或作为参数传递给其他函数,否则它们将不会被执行。

    js
    function sayHello() {
      // 定义了一个匿名函数,但由于未调用该函数,它不会执行任何操作
      let greeting = function () {
        console.log("Hello!");
      };
    }
    
    // 如果想要执行该匿名函数,可以添加一对括号来调用它
    function sayHello() {
      let greeting = function () {
        console.log("Hello!");
      };
      greeting(); // 调用匿名函数并输出 "Hello!"
    }

匿名函数使用方式

函数表达式

  • 函数表达式是指将一个匿名函数赋值给一个变量或常量。
  • 函数的形参和实参使用跟具名函数一致。
  • 除非将匿名函数赋值给变量或作为参数传递给其他函数,否则它们将不会被执行。
  • 函数表达式通常用于编写更简洁、可读性更强的代码,或在需要根据条件来动态创建函数时使用。
  • 使用场景:后期 web API 会使用。
js
let add = function (x, y) {
  return x + y;
};

// 通过变量名来调用匿名函数
let result = add(2, 3);
console.log(result); // 输出 5
js
let sortFunc;
if (userSortPreference === "ascending") {
  // 根据用户的排序偏好创建一个函数表达式,并将其赋值给变量 sortFunc
  sortFunc = function (a, b) {
    return a - b;
  };
} else {
  // 根据用户的排序偏好创建一个函数表达式,并将其赋值给变量 sortFunc
  sortFunc = function (a, b) {
    return b - a;
  };
}

// 使用该函数来对数组进行排序
array.sort(sortFunc);

立即执行函数

  • 立即执行函数是一个自我执行的匿名函数,它在定义后立即被执行。

  • 立即执行函数通常用于初始化代码或创建私有作用域。当我们需要一些代码在加载时立即执行时,可以使用立即执行函数来实现。

  • 场景介绍:避免全局变量之间的污染。可以避免在全局命名空间中创建不必要的变量,并且可以确保匿名函数不会与其他代码发生冲突。

js
(function () {
  console.log("Hello, world!");
})();
html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Immediate Function Demo</title>
    <script>
      // 定义了一个立即执行函数
      (function () {
        // 设置了一个名为 globalVar 的变量
        let globalVar = "Hello, world!";
        // 将其赋值给全局对象的 myGlobalVar 属性
        window.myGlobalVar = globalVar; // 将变量赋值给全局对象
      })();
    </script>
  </head>
  <body>
    <h1>Welcome to my website!</h1>
    <!-- 在页面加载时,我们可以使用 <%=myGlobalVar%> 语法将变量的值插入到 HTML 中。 -->
    <p>The value of my global variable is: <%=myGlobalVar%></p>
  </body>
</html>

注意

多个立即执行函数要用 ; 隔开,要不然会报错。

案例练习 - 转换时间案例

转换时间案例 (codepen.io)

  • 需求:用户输入秒数,可以自动转换为时分秒
  • 分析:
    • 用户输入总秒数(注意默认值)
    • 计算时分秒(封装函数)里面包含数字补 0
    • 打印输出
  • 计算公式:计算时分秒
    • 小时:h = parseInt(总秒数 / 60 / 60 % 24)
    • 分钟:m = parseInt(总秒数 / 60 % 60 )
    • 秒数:s = parseInt(总秒数 % 60)
html
<h1>转换时间</h1>

<form>
  <label for="seconds-input">总秒数</label>
  <input type="number" id="seconds-input" name="seconds" />
  <button type="button" onclick="handleInput()">转换</button>
</form>

<p>The time is: <span id="output">00:00:00</span></p>
js
function addZero(num) {
  return (num < 10 ? "0" : "") + num;
}

function convertSecondsToTime(seconds) {
  let h = parseInt((seconds / 60 / 60) % 24);
  let m = parseInt((seconds / 60) % 60);
  let s = parseInt(seconds % 60);
  return addZero(h) + ":" + addZero(m) + ":" + addZero(s);
}

function handleInput() {
  let secondsInput = document.getElementById("seconds-input");
  let output = document.getElementById("output");
  let seconds = parseInt(secondsInput.value || Math.floor(Math.random() * 3600 * 24));
  let timeString = convertSecondsToTime(seconds);
  output.innerHTML = timeString;
  secondsInput.value = "";
}